
/*

DEPENDENCIES:
-------------
Gimme.Helper
ext.for_each
-------------


IN THIS FILE:
-------------
Gimme.Events
Gimme.Events.capture_mouse
Gimme.Events.release_mouse
Gimme.Events.get_capture_target
Gimme.Events.Pseudo
Gimme.Events.Pseudo.mouseenter
Gimme.Events.Pseudo.mouseleave
Gimme.Events.Pseudo.mousewheel
-------------

*/

Gimme.Events = new function()
{
	var help = Gimme.Helper;	
	var capturingElem = null
	var rerouteFn = null;
	
	this.capture_mouse = function(elem, retargetToElement)
	{
		Gimme.Events.release_mouse();
		capturingElem = elem;

		if (retargetToElement !== false)
		{
			retargetToElement = true;
		}
		
		if (elem.setCapture !== undefined)
		{
			elem.setCapture(retargetToElement);
		}
		else
		{
			rerouteFn = function(e)
			{
				var target = e.target;
				var synthEvent = document.createEvent('MouseEvents');
				synthEvent.initMouseEvent(
					e.type							// type
					,e.bubbles						// can bubble?
					,e.cancelable					// cancelable?
					,window							// always window here
					,e.detail						// mouse click count
					,e.screenX						// event's screen x coordinate (copied from the passed in event)
					,e.screenY						// event's screen y coordinate (copied from the passed in event)
					,e.clientX						// event's client x coordinate (copied from the passed in event)
					,e.clientY						// event's client y coordinate (copied from the passed in event)
					,e.ctrlKey						// whether or not the ctrl key was pressed during the event (copied from the passed in event)
					,e.altKey						// whether or not the alt key was pressed during the event (copied from the passed in event)
					,e.shiftKey						// whether or not the shift key was pressed during the event (copied from the passed in event)
					,e.metaKey						// whether or not the meta key was pressed during the event (copied from the passed in event)
					,e.button						// indicates which button (if any) caused the mouse event (copied from the passed in event)
					,e.relatedTarget				// relatedTarget (only applicable for 'mouseover' and 'mouseout' events)
				);
					
				// these 3 lines fix a FF3 bug (not sure if the bug still exists in FF > 3, but they seemed
				// resistant to fixing it when I filed the bug on bugzilla.mozilla.org) -- redefining the
				// .pageX and .pageY getters this way doesn't hurt non Mozilla browsers (and IE doesn't take
				// this code page, so that's not an issue either), but just in case, we make sure the browser
				// support .__defineGetter__ before we try to do this.
				if (synthEvent.__defineGetter__)
				{
					var scrollPos = Gimme.Screen.get_scroll_pos();
					synthEvent.__defineGetter__('pageX', function() { return this.clientX + scrollPos.x; });
					synthEvent.__defineGetter__('pageY', function() { return this.clientY + scrollPos.y; });
				}

				document.removeEventListener(e.type, rerouteFn, true);
				synthEvent.captureTarget = target;
				
				// Determine which element we should dispatch the event to:
				//
				// If we're NOT retargeting the event to the capturing element (e.g. descendants of the capturing
				// element can receive the event), then we should dispatch to event's target (so long as the target
				// is a descendant of the capturing element).
				//
				// Otherwise, we should dispatch to the capturing element itself.
				//
				var dispatchElem = capturingElem;
				if (!retargetToElement)
				{
					if (Gimme(capturingElem).has(target))
					{
						dispatchElem = target;
					}
				}
				dispatchElem.dispatchEvent(synthEvent);

				if (rerouteFn !== null)
				{
					document.addEventListener(e.type, rerouteFn, true);
				}
				delete synthEvent.captureTarget;
				
				// stop the original event from propagating, otherwise we'll end up with double handler execution
				e.stopPropagation();
			}
			
			document.addEventListener('mouseover', rerouteFn, true);
			document.addEventListener('mouseout', rerouteFn, true);
			document.addEventListener('mousemove', rerouteFn, true);
			document.addEventListener('mouseup', rerouteFn, true);
			document.addEventListener('mousedown', rerouteFn, true);
			document.addEventListener('click', rerouteFn, true);
			document.addEventListener('dblclick', rerouteFn, true);
		}
		
		return this;
	};
	
	this.release_mouse = function()
	{
		if (capturingElem !== null)
		{
			if (capturingElem.releaseCapture !== undefined)
			{
				capturingElem.releaseCapture();
			}
			else
			{
				document.removeEventListener('mouseover', rerouteFn, true);
				document.removeEventListener('mouseout', rerouteFn, true);
				document.removeEventListener('mousemove', rerouteFn, true);
				document.removeEventListener('mouseup', rerouteFn, true);
				document.removeEventListener('mousedown', rerouteFn, true);
				document.removeEventListener('click', rerouteFn, true);
				document.removeEventListener('dblclick', rerouteFn, true);
			}
			
			capturingElem = rerouteFn = null;
		}
		
		return this;
	};

	this.get_capture_target = function(_evt)
	{
		return _evt.captureTarget || _evt.srcElement || _evt.target;
	};
	

	this.Pseudo = new function()
	{
		var prevPointerId = null;
		var disableClick = {};
		var handlerHash = {};	
		var touchesHash = {};
		var touchEventNameTranslation =
		{
			touchstart: 'MSPointerDown',
			touchend: 'MSPointerUp',
			touchmove: 'MSPointerMove',
			touchcancel: 'MSPointerCancel'
		};

		// PseudoEvent class 
		function PseudoEvent(name, fn1, fn2)
		{
			this.pseudoName = name;
			this.getNativeName = fn1;
			this.getProxiedHandler = fn2;
		}
		PseudoEvent.prototype.add = function(gim, handler, useCapture)
		{
			gim.add_event(this.getNativeName(this.pseudoName), this.getProxiedHandler(handler, gim), useCapture, true);
		};
		PseudoEvent.prototype.remove = function(gim, handler, useCapture)
		{
			gim.remove_event(this.getNativeName(this.pseudoName), this.getProxiedHandler(handler, gim), useCapture, true);
		};
		//
		
		// helper function to determine if one element (descendant) descends from another (ancestor)
		function isAnAncestorOf(ancestor, descendant, generation)
		{
			if (ancestor === descendant)
			{
				return false;
			}
			
			var gen = 0;
			while (descendant && descendant != ancestor)
			{
				gen++;
				descendant = descendant.parentNode;
			}
			
			generation = generation || gen;
			return descendant === ancestor && generation === gen;		
		}
		
		// proxy function for the 'mouseenter' pseudoEvent 
		function mouseEnter(handler)
		{				
			var key = help.get_guid(handler);
			var proxy = handlerHash[key];
			if (!proxy)
			{
				proxy = handlerHash[key] = function(e)
				{
					var relTarget = e.relatedTarget;
					if (!relTarget || this === relTarget || isAnAncestorOf(this, relTarget))
					{
						return;
					}
			
					handler.call(this, e);
				};
			}
			return proxy;
		}
		
		// proxy function for the 'mousewheel' pseudoEvent (used by Mozilla/Gecko)
		function mouseWheel(handler)
		{
			if (!Gimme.Browser.is_gecko)
			{
				return handler;
			}
			
			var key = help.get_guid(handler);
			var proxy = handlerHash[key];
			if (!proxy)
			{
				proxy = handlerHash[key] = function(e)
				{
					// Gecko's "detail" is in terms of lines scrolled (typically 3), and has a 
					// sign opposite that of IE's wheelDelta (which uses 120-unit increments).
					// This maps back into IE's style of units.
					e.wheelDelta = -40 * e.detail;
					handler.call(this, e);
					e.wheelDelta = null;
				};
			}
			return proxy;
		}
		
		// proxy function for all pseudo touch events (used by Win8/IE)
		function normalizeTouch(handler)
		{
			if (!Gimme.Browser.is_ie)
			{
				return handler;
			}
			
			var key = help.get_guid(handler);
			var proxy = handlerHash[key];
			if (!proxy)
			{
				proxy = handlerHash[key] = function(e)
				{
					if (e.pointerType === 2)
					{
						if (e.type === 'MSPointerUp' || e.type === 'MSPointerCancel')
						{
							delete touchesHash[e.pointerId];
						}
						else
						{
							var mousePos = Gimme.Screen.get_mouse_pos(e);
							touchesHash[e.pointerId] =
							{
								_$targetId: help.get_guid(e.target),
								identifier: e.pointerId,
								pageX: mousePos.x,
								pageY: mousePos.y
							};
						}
			
						// augment the native event with a .touches (array) property
						// and a .targetTouches (array) property, before calling the handler
						var key, touches = [], targetTouches = [];
						for (key in touchesHash)
						{
							if (touchesHash.hasOwnProperty(key) && touchesHash[key])
							{
								var t = touchesHash[key];
								touches.push(t);
								if (t._$targetId === help.get_guid(e.target))
								{
									targetTouches.push(t);
								}
							}
						}
						e.touches = touches;
						e.targetTouches = targetTouches;

						// If we're dealing with a "touchstart" event, we'd better "fix" IE's preventDefault implementation
						// with out own, or else we'll have inconsistent behavior between IE10 and iOS/Android
						e.type === 'MSPointerDown' && (e.preventDefault = customPreventDefault);
						
						// call the original handler
						handler.call(this, e);

						// handler has returned, so we don't need these anymore
						e.touches =
						e.targetTouches = null;
					}
				};				
			}
			return proxy;
		}

		function customPreventDefault()
		{
			// If e.preventDefault() was called form within a "touchstart" handler (and that fact that this function is executing
			// means that it was), we need to disable click events for this event propagation cycle.
			// We start by making a note of the pointerId of this event and stuffing it in a hash to let us know that clicks that
			// result from this event should be thwarted.
			disableClick[this.pointerId] = true;

			// We'll need to track "touchend" so that we know when the corresponding "up" of this event has occurred.
			document.addEventListener('MSPointerUp', trackPointerUp, true);

			// And we need some custom logic to manage whether or not a click event should be allowed through the DOM.
			// Note that we used event capturing here so that we can snatch this event at the document level before 
			// anything else has a chance to respond to it.
			document.addEventListener('click', manageClick, true);

			// Finally, call the original preventDefault and use "this" (the current event) as the context
			return MSPointerEvent.prototype.preventDefault.call(this);
		}

		function trackPointerUp(e)
		{
			// Since "click" events don't have pointerIds, we need to keep track of the last pointerId that went "up" so to speak.
			prevPointerId = e.pointerId;

			// And if the pointerId of the event that went "up" is in the disableClick hash, here we'll set it to false
			// to indicate that it is stale and needs to be removed once clicks are re-enabled on the page.
			disableClick[prevPointerId] && (disableClick[prevPointerId] = false);
		}

		function manageClick(e)
		{
			var key, count = 0;
			for (key in disableClick)
			{
				// key is a string, so if we want to compare it to prevPointerId (which we do) we'd better convert it
				key = parseInt(key);

				// Anything in the disableClick hash marked false is stale and can (should) be removed.
				// Anything that shouldn't be removed is an active item in the hash, so we increment the counter.
				disableClick[key] === false && delete disableClick[key] || count++;

				// If the current key matches the pointerId of the last "up" we need to prevent any "would-be" clicks.
				// We do this by stopping the event from propagating through the DOM and by preventing the default action.
				// The preventDefault call is needed because anytime there may have been a preventDefault call from within
				// a click handler, iOS/Android would ensure that the default action were indeed prevented.  Ours is a custom
				// implementation though, which works by not even allowing the click event to propagate to its target.  Thus,
				// if a someone put a preventDefault call in a click handler, it would never be reached.  Calling it here solves
				// that problem.  And if there WEREN'T a preventDefault call in the click handler, no big deal, since we're
				// not allowing clicks anyway.  Makes perfect sense right? :-)
				key === prevPointerId && (e.stopPropagation() || e.preventDefault());
			}

			// If the disableClick hash is empty, we can unwire our "click-thwarting" handlers, thus re-enabling clicks.
			if (count === 0)
			{
				document.removeEventListener('MSPointerUp', trackPointerUp, true);
				document.removeEventListener('click', manageClick, true);
			}
		}

		
		// dummy function to ensure touches are removed from the touchesHash on touchend or touchcancel			
		function dummyTouch(e)
		{
			//console.log('dummyTouch: ' + e.touches.length);
		}
		
		// return a new dictionary made up of pseuodEvents that Gimme knows about
		return {
			mouseenter: new PseudoEvent('mouseenter',
				// retuns the native event name Gimme should use
				function(pseudoName)
				{
					return 'mouseover';
				},
				// returns a reference to the actual function Gimme should wire up
				function(handler)
				{
					return mouseEnter(handler);
				}
			),
			
			mouseleave: new PseudoEvent('mouseleave',
				function(pseudoName)
				{
					return 'mouseout';
				},
				function(handler)
				{
					return mouseEnter(handler);
				}
			),
			
			mousewheel: new PseudoEvent('mousewheel',
				function(pseudoName)
				{
					return Gimme.Browser.is_gecko ? 'DOMMouseScroll' : pseudoName;
				},
				function(handler)
				{
					return mouseWheel(handler);
				}
			),
			
			touchstart: new PseudoEvent('touchstart',
				function(pseudoName)
				{
					return Gimme.Browser.is_ie ? touchEventNameTranslation[pseudoName] : pseudoName;
				},
				function(handler, gim)
				{
					// adding a dummy pseudoEvent will cause dummyTouch to be proxied through
					// the normalizeTouch function, where, if necessary, touches will be removed
					// from the touchesHash if the event type was 'MSPointerUp' or 'MSPointerCancel'
					// note: Gimme is smart enough to only add this dummyTouch handler once, regardless
					// of how many time this is called.
					Gimme(document)
						.add_event('touchend', dummyTouch, true)
						.add_event('touchcancel', dummyTouch, true);
					return normalizeTouch(handler);
				}
			),
			
			touchend: new PseudoEvent('touchend',
				function(pseudoName)
				{
					return Gimme.Browser.is_ie ? touchEventNameTranslation[pseudoName] : pseudoName;
				},
				function(handler)
				{
					return normalizeTouch(handler);
				}
			),

			touchmove: new PseudoEvent('touchmove',
				function(pseudoName)
				{
					return Gimme.Browser.is_ie ? touchEventNameTranslation[pseudoName] : pseudoName;
				},
				function(handler)
				{
					return normalizeTouch(handler);
				}
			),

			touchcancel: new PseudoEvent('touchcancel',
				function(pseudoName)
				{
					return Gimme.Browser.is_ie ? touchEventNameTranslation[pseudoName] : pseudoName;
				},
				function(handler)
				{
					return normalizeTouch(handler);
				}
			)
		};
	};
};
